package com.hzlj.position.locate.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.fz.common.base.core.JsonUtils;
import com.fz.common.base.core.date.DateUtils;
import com.fz.common.base.dto.PageParamDTO;
import com.fz.common.base.dto.QueryPageResultDTO;
import com.fz.common.base.exception.PubException;
import com.fz.common.dict.annotation.DictConvert;
import com.fz.common.redis.annotation.SyncLock;
import com.google.common.collect.Lists;
import com.hzlj.common.message.common.dto.message.MessageNoticeResultDTO;
import com.hzlj.common.message.common.dto.message.MessageNoticeUserDTO;
import com.hzlj.position.config.common.dto.common.LocateTimesDTO;
import com.hzlj.position.config.common.dto.common.PositionAddExtDTO;
import com.hzlj.position.config.common.dto.common.ReportLocateDTO;
import com.hzlj.position.config.common.dto.jzJbxxDeviceBind.JzJbxxDeviceBindSimpleQueryParamDTO;
import com.hzlj.position.config.common.dto.jzJbxxDeviceBind.JzJbxxDeviceBindWithLocateTimesDTO;
import com.hzlj.position.config.common.enums.ReportType;
import com.hzlj.position.config.common.enums.WarningType;
import com.hzlj.position.config.config.PositionConfig;
import com.hzlj.position.config.service.JzJbxxDeviceBindService;
import com.hzlj.position.config.service.NoticeConfigService;
import com.hzlj.position.locate.common.common.ReportTimesDTO;
import com.hzlj.position.locate.common.dto.messageToday.MessageTodayAddDTO;
import com.hzlj.position.locate.common.dto.position.PositionQueryResultDTO;
import com.hzlj.position.locate.common.dto.reportCommon.ReportHandleDTO;
import com.hzlj.position.locate.common.dto.reportInitiative.ReportInitiativeQueryParamDTO;
import com.hzlj.position.locate.common.dto.reportInitiative.ReportInitiativeWithJzJbxxDTO;
import com.hzlj.position.locate.common.dto.reportInitiativeSetting.ReportInitiativeSettingQueryResultDTO;
import com.hzlj.position.locate.common.enums.ReportStatus;
import com.hzlj.position.locate.common.enums.SignStatus;
import com.hzlj.position.locate.common.model.ReportInitiative;
import com.hzlj.position.locate.converter.ReportInitiativeConverter;
import com.hzlj.position.locate.dao.ReportInitiativeDao;
import com.hzlj.position.locate.service.*;
import com.hzlj.sqjz.config.common.dto.common.handle.HandleCommonQueryResultDTO;
import com.hzlj.sqjz.config.common.enums.HandleStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;

import static com.hzlj.position.config.config.IdConfig.POSITION_ID_GEN;

/**
 * 通信联络核查-人脸签到(ReportInitiative)表服务接口
 *
 * @author lifh
 */
@Slf4j
@Service
public class ReportInitiativeServiceImpl implements ReportInitiativeService, MessageTemplateBussService {
    @Resource
    private ReportInitiativeDao reportInitiativeDao;
    @Resource
    private JzJbxxDeviceBindService jzJbxxDeviceBindService;
    @Resource
    private PositionAddTaskService positionAddTaskService;
    @Resource
    private ReportSyncService reportSyncService;
    @Resource
    private ReportInitiativeSettingService reportInitiativeSettingService;
    @Resource
    private PositionConfig positionConfig;
    @Resource
    private MessageTodayService messageTodayService;
    @Resource
    private NoticeConfigService noticeConfigService;


    @Override
    @DictConvert
    public List<ReportInitiativeWithJzJbxxDTO> listReportInitiative(ReportInitiativeQueryParamDTO dto) {
        return this.reportInitiativeDao.listReportInitiative(dto);
    }

    /**
     * 查询列表：分页
     */
    @Override
    @DictConvert
    public QueryPageResultDTO<ReportInitiativeWithJzJbxxDTO> pageReportInitiative(ReportInitiativeQueryParamDTO dto, PageParamDTO page) {
        return this.reportInitiativeDao.pageReportInitiative(dto, page);
    }

    /**
     * 同步签到
     * 1、同步签到信息
     * 2、设置同步状态
     */
    @Override
    @SyncLock(lockMs = 300000)
    public int sync() {
        boolean success = true;
        Date now = new Date();
        //检测是否已经同步
        boolean synced = reportSyncService.synced(null, ReportType.INITIATIVE.getCode(), now);
        if (synced) {
            return 0;
        }
        List<JzJbxxDeviceBindWithLocateTimesDTO> binds = jzJbxxDeviceBindService.listJzJbxxDeviceBind4LocateTimes(
                new JzJbxxDeviceBindSimpleQueryParamDTO()
                        .setMkh(!positionConfig.getInitiative().getExcludeMkh())
                        .setQzcs(!positionConfig.getInitiative().getExcludeQzcs())
        );
        log.info("人脸签到同步，预计需同步人员：size={}", binds.size());

        //分组
        for (List<JzJbxxDeviceBindWithLocateTimesDTO> bindsSplit : CollectionUtil.split(binds, 500)) {
            //转换
            List<Pair<JzJbxxDeviceBindWithLocateTimesDTO, ReportInitiative>> initiatives = bindsSplit.stream().map(e -> {
                        ReportInitiative initiative = this.convert(e, now);
                        if (initiative == null) {
                            return null;
                        }
                        //去重
                        boolean exists = this.reportInitiativeDao.existsReportInitiativeByJzId(initiative.getJzId(), initiative.getBelongDay());
                        if (exists) {
                            log.info("人脸签到同步，不需要重复同步：jzId={},xm={}", e.getJzId(), e.getXm());
                            return null;
                        }
                        return new Pair<>(e, initiative);
                    })
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            log.info("人脸签到同步，批次提交同步人员：size={}", initiatives.size());
            try {
                if (ObjectUtil.isEmpty(initiatives)) {
                    continue;
                }
                this.reportInitiativeDao.saveBatch(
                        initiatives.stream().map(Pair::getValue).collect(Collectors.toList())
                );
                //保存时间点
                syncNotice(initiatives);
                log.info("人脸签到同步，批次提交同步人员成功：size={}", initiatives.size());
            } catch (Exception e) {
                success = false;
                log.info("人脸签到同步，批次提交同步人员出错：size={}", initiatives.size(), e);
            }
        }
        //设置同步状态
        if (success) {
            this.reportSyncService.upsert(null, ReportType.INITIATIVE.getCode(), now);
        }
        return binds.size();
    }

    @Override
    public void sync(String jzId) {
        this.syncJzry(jzId);
    }

    /**
     * 上报
     */
    @Override
    @SyncLock(key = "#locate.jzId")
    @Transactional(rollbackFor = Exception.class)
    public void reportLocate(ReportLocateDTO locate) {
        //获取绑定记录
        Pair<JzJbxxDeviceBindWithLocateTimesDTO, ReportInitiative> pair = this.syncJzry(locate.getJzId());
        ReportInitiative report = pair.getValue();
        JzJbxxDeviceBindWithLocateTimesDTO bind = pair.getKey();
        //上报时间段校验
        ReportTimesDTO.Item item = report.getReportTimes().match(
                noticeConfigService.initiativeNotices(bind.getJgCode()).getNoticeComplement(),
                noticeConfigService.initiativeNotices(bind.getJgCode()).getNoticeComplementMinutes(),
                locate.getReportTime()
        );
        if (item == null) {
            log.warn("人脸签到：不在签到时间段，或者不满足补签要求={},times={}", locate, JsonUtils.obj2json(report.getReportTimes()));
            throw new PubException("不在签到时间段");
        }
        //同步到positionTask中
        PositionQueryResultDTO positionResult = this.positionAddTaskService.add(locate
                .setId(report.getId())
                .setDeviceCode(bind.getDeviceCode())
                .setChannel(bind.getChannel())
                .setChannelType(bind.getChannelType())
                .setReportType(ReportType.INITIATIVE.getCode())
                .setPositionExt(new PositionAddExtDTO()
                        .setFileIds(locate.getFileIds())
                        .setTimePeriod(item.getStartTime() + "-" + item.getEndTime())
                )
        );

        //是否在时间段内签到
        boolean inTime = item.checkInTime(locate.getReportTime());
        //上次的签到情况
        String signStatus = item.signStatus();
        ReportTimesDTO.ItemPosition position = item.createOrGetPosition(locate.getReportTime());
        //设置签到情况
        position.setReportTime(locate.getReportTime());
        position.setReportAddress(locate.getReportAddress());
        position.setLatitude(locate.getLatitude());
        position.setLongitude(locate.getLongitude());
        position.setFileIds(locate.getFileIds());
        position.setSignStatus(inTime ? SignStatus.DONE.getStatus() : SignStatus.COMPLEMENT.getStatus());
        if (WarningType.isCross(positionResult.getWarningType())
                && positionConfig.getInitiative().getDisplayCrossInReportTimes()) {
            position.setSignStatus(SignStatus.CROSS.getStatus());
        }
        //更新签到情况
        ReportInitiative update = BeanUtil.copyProperties(position, ReportInitiative.class);
        update.setId(report.getId());
        update.setBelongDay(DateUtil.beginOfDay(locate.getReportTime()));
        update.setReportTimes(report.getReportTimes());
        //设置标记
        ReportInitiativeConverter.setReportTimesFlag(report, update, update.getReportTime());
        //次数
        update.setSuccessAmt(report.getSuccessAmt() + 1);
        update.setNeverAmt(report.getNeverAmt());
        if (ObjectUtil.isEmpty(signStatus)) {//如果这个时间段内没有签到，那么次数要-1
            update.setNeverAmt(Math.max(report.getNeverAmt() - 1, 0));
        }
        //状态
        if (update.getNeverAmt() == 0) {
            update.setReportStatus(ReportStatus._DONE.getStatus());
        } else {
            update.setReportStatus(ReportStatus._SUCCESS.getStatus());
        }
        //更新
        this.reportInitiativeDao.updateReportInitiative(update);
    }

    @Override
    public void delete(String jzId, Date date) {
        this.reportInitiativeDao.delete(jzId, DateUtil.beginOfDay(date));
    }


    @Override
    public void handle(ReportHandleDTO dto) {
        this.reportInitiativeDao.handle(dto);
    }

    @Override
    public HandleCommonQueryResultDTO getHandleResult(String id, Date belongDay) {
        return this.reportInitiativeDao.getHandleResult(id, belongDay);
    }


    private Pair<JzJbxxDeviceBindWithLocateTimesDTO, ReportInitiative> syncJzry(String jzId) {
        Date now = new Date();
        JzJbxxDeviceBindWithLocateTimesDTO bind = this.jzJbxxDeviceBindService.getJzJbxxDeviceBind4LocateTimes(jzId);
        //已有记录则不同步
        ReportInitiative report = this.reportInitiativeDao.getReportInitiativeByJzId(jzId, now);
        if (report != null) {
            return new Pair<>(bind, report);
        }
        //没有则保存
        report = this.convert(bind, now);
        if (report == null) {
            throw new PubException("未配置签到时间段");
        }
        this.reportInitiativeDao.save(report);
        //保存时间点
        syncNotice(Lists.newArrayList(new Pair<>(bind, report)));
        return new Pair<>(bind, report);
    }

    /**
     * bind信息转换成ReportInitiative
     */
    private ReportInitiative convert(JzJbxxDeviceBindWithLocateTimesDTO bind, Date now) {
        try {
            if (ObjectUtil.isEmpty(bind)) {
                return null;
            }
            LocateTimesDTO locateTimes = bind.getLocateTimes();
            if (ObjectUtil.isEmpty(locateTimes)) {
                locateTimes = new LocateTimesDTO();
                //查询默认的
                List<ReportInitiativeSettingQueryResultDTO> settings = this.reportInitiativeSettingService.getSysReportInitiativeSetting(bind.getJgCode());
                if (ObjectUtil.isEmpty(settings)) {
                    return null;
                }
                locateTimes.addAll(
                        settings.stream().map(e -> new LocateTimesDTO.Item(e.getId(), e.getStartTime(), e.getEndTime())).collect(Collectors.toList())
                );
            }
            ReportInitiative report = new ReportInitiative();
            report.setId(POSITION_ID_GEN.nextIdStr());
            report.setJzId(bind.getJzId());
            report.setBelongDay(DateUtil.beginOfDay(now));
            report.setCreateTime(now);
            //未签到状态
            report.setReportStatus(ReportStatus._NEVER.getStatus());
            //签到时间段设置
            ReportTimesDTO reportTimes = new ReportTimesDTO();
            reportTimes.addAll(locateTimes.stream().map(e -> new ReportTimesDTO.Item(e.getStartTime(), e.getEndTime())).collect(Collectors.toList()));
            report.setReportTimes(reportTimes);
            report.setTotalAmt(reportTimes.size());
            report.setNeverAmt(reportTimes.size());
            report.setSuccessAmt(0);
            return report;
        } catch (Exception e) {
            log.error("人脸签到同步，转化数据出错：bind={}", JsonUtils.obj2json(bind), e);
            return null;
        }

    }

    /**
     * 校验是否需要发送通知
     */
    @Override
    public boolean checkNeedSend(String messageTemplate,
                                 String serviceId,
                                 Date belongDay,
                                 Map<String, Object> messageParams,
                                 Map<String, Object> checkParams) {
        ReportInitiativeWithJzJbxxDTO reportInitiative = this.reportInitiativeDao.getReportInitiative(serviceId, belongDay);
        if (reportInitiative == null
                || ObjectUtil.isEmpty(checkParams.get("endTime"))
                || ObjectUtil.isEmpty(reportInitiative.getReportTimes())) {
            return false;
        }

        //处理handle状态
        if (messageTemplate.equals("report-initiative-no-report-handle")) {
            if (!reportInitiative.getReportStatus().equals(ReportStatus._DONE.getStatus())) {
                log.info("签到未完成，需要更新handleStatus状态:id={},belongDay={}", serviceId, belongDay);
                this.reportInitiativeDao.updateHandleStatus(serviceId, belongDay, HandleStatus.WAIT);
            }
            return false;
        }

        ReportTimesDTO.Item item = reportInitiative.getReportTimes().match(checkParams.get("endTime").toString());
        //未签到
        return item != null && !item.signed();
    }

    @Override
    public void updateNoticeResult(String messageTemplate,
                                   String serviceId,
                                   Date belongDay,
                                   Map<String, Object> checkParams,
                                   MessageNoticeResultDTO notice) {
        Date now = new Date();
        //修改补签状态
        if (messageTemplate.equals("report-initiative-complement")) {
            ReportInitiativeWithJzJbxxDTO initiative = this.reportInitiativeDao.getReportInitiative(serviceId, belongDay);
            ReportTimesDTO.Item item = initiative.getReportTimes().match(checkParams.get("endTime").toString());
            Integer complementMinutes = noticeConfigService.initiativeNotices(initiative.getJgCode()).getNoticeComplementMinutes();

            if (item == null
                    || item.signed()
                    || item.complemented()
                    || !item.checkInComplementTime(now, complementMinutes)) {
                return;
            }
            ReportInitiative update = new ReportInitiative();
            update.setId(serviceId);
            update.setBelongDay(belongDay);
            update.setReportTimes(initiative.getReportTimes());
            ReportTimesDTO.ItemPosition position = item.createOrGetPosition(now);
            position.setComplement(!notice.isFail());
            position.setComplementTime(new Date());
            position.setComplementFailMsg(notice.getStatusMsg());
            this.reportInitiativeDao.updateReportInitiative(update);
        }
    }

    /**
     * 发送通知
     */
    private void syncNotice(List<Pair<JzJbxxDeviceBindWithLocateTimesDTO, ReportInitiative>> reportInitiatives) {
        if (ObjectUtil.isEmpty(reportInitiatives)) {
            return;
        }
        //补签通知
        List<MessageTodayAddDTO> jzries = reportInitiatives.stream()
                .filter(e -> noticeConfigService.initiativeNotices(e.getKey().getJgCode()).getNoticeComplement())
                .flatMap(e -> {
                    ReportInitiative value = e.getValue();
                    return value.getReportTimes().stream().map(item -> {
                        Date noticeTime = DateUtils.parseFixedHHmm(value.getBelongDay(), item.getEndTime());
                        //如果与24：00相差不足30分钟，就不需要补签
                        if (DateUtil.between(noticeTime, DateUtil.endOfDay(noticeTime), DateUnit.MINUTE) < 30) {
                            return null;
                        }
                        return convert(e.getKey(), value, item, offsetNoticeTime(noticeTime, false))
                                .setMessageTemplate("report-initiative-complement")
                                .setUser(MessageNoticeUserDTO.of(false, value.getJzId()));
                    }).filter(Objects::nonNull);
                }).collect(Collectors.toList());
        messageTodayService.sendAsync(jzries);

        //提前通知矫正对象还未签到
        jzries = reportInitiatives.stream()
                .filter(e -> noticeConfigService.initiativeNotices(e.getKey().getJgCode()).getNoticeJzryBefore())
                .flatMap(e -> {
                    ReportInitiative value = e.getValue();
                    return value.getReportTimes().stream().map(item -> {
                        return convert(e.getKey(), value, item, null)
                                .setMessageTemplate("report-initiative-before")
                                .setUser(MessageNoticeUserDTO.of(false, value.getJzId()));
                    });
                }).collect(Collectors.toList());
        messageTodayService.sendAsync(jzries);


        //未上报处理通知
        jzries = reportInitiatives.stream()
                .filter(e -> noticeConfigService.initiativeNotices(e.getKey().getJgCode()).getHandleIfNoReport())
                .map(e -> {
                    ReportInitiative value = e.getValue();
                    Optional<ReportTimesDTO.Item> maxItem = value.getReportTimes().stream().max(Comparator.comparing(ReportTimesDTO.Item::getEndTime));
                    return maxItem.map(item -> convert(e.getKey(), value, item, DateUtils.parseFixedHHmm(value.getBelongDay(), item.getEndTime()))
                            .setMessageTemplate("report-initiative-no-report-handle")
                            .setUser(MessageNoticeUserDTO.of(false, value.getJzId()))).orElse(null);
                }).collect(Collectors.toList());
        messageTodayService.sendAsync(jzries);


        //提前通知工作人员哪些对象没签到
        List<MessageTodayAddDTO> gzries = reportInitiatives.stream()
                .filter(e -> noticeConfigService.initiativeNotices(e.getKey().getJgCode()).getNoticeGzryBefore())
                .flatMap(e -> {
                    ReportInitiative value = e.getValue();
                    return value.getReportTimes().stream().map(item -> {
                        return convert(e.getKey(), value, item, null)
                                .setMessageTemplate("report-initiative-before-gzry")
                                .setUser(MessageNoticeUserDTO.of(true, e.getKey().getJgCode(), noticeConfigService.defaultMessageNoticeAuths(e.getKey().getJgCode())));
                    });
                }).collect(Collectors.toList());
        messageTodayService.sendAsync(gzries);

        //完成后通知工作人员哪些对象没签到
        gzries = reportInitiatives.stream()
                .filter(e -> noticeConfigService.initiativeNotices(e.getKey().getJgCode()).getNoticeGzryAfter())
                .flatMap(e -> {
                    ReportInitiative value = e.getValue();
                    return value.getReportTimes().stream().map(item -> {
                        Date noticeTime = DateUtils.parseFixedHHmm(value.getBelongDay(), item.getEndTime());
                        return convert(e.getKey(), value, item, offsetNoticeTime(noticeTime, false))
                                .setMessageTemplate("report-initiative-after-gzry")
                                .setUser(MessageNoticeUserDTO.of(true, e.getKey().getJgCode(), noticeConfigService.defaultMessageNoticeAuths(e.getKey().getJgCode())));
                    });
                }).collect(Collectors.toList());
        messageTodayService.sendAsync(gzries);
    }

    private MessageTodayAddDTO convert(JzJbxxDeviceBindWithLocateTimesDTO bind,
                                       ReportInitiative initiative,
                                       ReportTimesDTO.Item item,
                                       Date noticeTime) {
        Integer noticeJzryMinutes = noticeConfigService.initiativeNotices(bind.getJgCode()).getNoticeJzryMinutes();
        if (noticeTime == null) {
            noticeTime = DateUtil.offset(DateUtils.parseFixedHHmm(initiative.getBelongDay(), item.getEndTime()), DateField.MINUTE, -noticeJzryMinutes);
            noticeTime = offsetNoticeTime(noticeTime, true);
        }
        return new MessageTodayAddDTO()
                .setBelongDay(initiative.getBelongDay())
                .setMessageParams(JsonUtils.obj2json(MapUtil.<String, Object>builder()
                        .put("jzryXm", bind.getXm())
                        .put("jzrySjh", bind.getSjh())
                        .put("startTime", item.getStartTime())
                        .put("endTime", item.getEndTime())
                        .put("belongDay", DateUtil.formatDate(initiative.getBelongDay()))
                        .put("spotCheckTime", DateUtil.formatDateTime(noticeTime))
                        .put("spotCheckDay", DateUtil.formatDate(noticeTime))
                        .build()
                ))
                .setCheckParams(JsonUtils.obj2json(MapUtil.<String, Object>builder()
                        .put("endTime", item.getEndTime()).build()
                ))
                .setNoticeTime(noticeTime)
                .setServiceId(initiative.getId());
    }

    private Date offsetNoticeTime(Date noticeTime, boolean ahead) {
        int offset = RandomUtil.randomInt(0, 5);
        return DateUtil.offset(noticeTime, DateField.MINUTE, ahead ? -offset : offset);
    }
}
